<!DOCTYPE html>
<link rel=author href="mailto:jarhar@chromium.org">
<link rel=help href="https://github.com/whatwg/html/pull/7991">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>

<script>
function isAsciiLowerAlpha(codePoint) {
  return codePoint >= 0x61 && codePoint <= 0x7A;
}
function isAsciiUpperAlpha(codePoint) {
  return codePoint >= 0x41 && codePoint <= 0x5A;
}
function isAsciiAlpha(codePoint) {
  return isAsciiLowerAlpha(codePoint) || isAsciiUpperAlpha(codePoint);
}
function isAsciiDigit(codePoint) {
  return codePoint >= 0x30 && codePoint <= 0x39;
}
function isAsciiWhitespace(codePoint) {
  return codePoint == 0x9 || codePoint == 0xA || codePoint == 0xC || codePoint == 0xD || codePoint == 0x20;
}

function debugString(str) {
  const codePoints = [];
  for (const c of str) {
    codePoints.push(c.codePointAt(0));
  }
  return `code points: ${JSON.stringify(codePoints)}, string: "${str}"`;
}

const validCustomElementNames = [
  'annotation-xml-custom',
];
const invalidCustomElementNames = [
  '',
  'annotation-xml',
  'color-profile',
  'font-face',
  'font-face-src',
  'font-face-uri',
  'font-face-format',
  'font-face-name',
  'missing-glyph',
];

const testCodePoints = [0x1F171, 0x1F196, 0x10000];
for (let i = 0; i < 0x80; i++) {
  testCodePoints.push(i);
}

const elementLocalNameRegex = /^(?:[A-Za-z][^\0\t\n\f\r\u0020/>]*|[:_\u0080-\u{10FFFF}][A-Za-z0-9-.:_\u0080-\u{10FFFF}]*)$/u;

function isValidCustomElementName(str) {
  if (!str.length) {
    return false;
  }

  if (!str.includes('-')) {
    return false;
  }

  let first = true;
  for (const c of str) {
    const codePoint = c.codePointAt(0);
    if (first) {
      if (!isAsciiLowerAlpha(codePoint)) {
        return false;
      }
      first = false;
    }
    if (isAsciiUpperAlpha(codePoint)) {
      return false;
    }
  }

  return elementLocalNameRegex.test(str);
}

// In order to test the branching logic of valid element local names and the
// requirement of having a '-' character, this method generates different
// variations of potential custom element names given two code points.
function createStringWithSeparatorMode(codePoint, prefix, separatorMode) {
  const str = String.fromCodePoint(codePoint);
  if (separatorMode == 0) {
    return `${prefix}${str}`;
  } else if (separatorMode == 1) {
    return `${prefix}-${str}`;
  } else if (separatorMode == 2) {
    return `${prefix}${str}-element`;
  }
}

for (const prefix of ['', 'a', 'A', ' ', '\0']) {
  for (const codePoint of testCodePoints) {
    for (const separatorMode of [0, 1, 2]) {
      const str = createStringWithSeparatorMode(
        codePoint, prefix, separatorMode);
      if (isValidCustomElementName(str)) {
        validCustomElementNames.push(str);
      } else {
        invalidCustomElementNames.push(str);
      }
    }
  }
}

let nextClassNumber = 1;
function createElementClass() {
  const name = `CustomElement${nextClassNumber++}`;
  const newClass = function() {};
  newClass.prototype = HTMLElement;
  return newClass;
}

promise_test(async t => {
  for (const validName of validCustomElementNames) {
    try {
      const newClass = createElementClass();
      customElements.define(validName, newClass);
      await customElements.whenDefined(validName);
    } catch (error) {
      assert_unreached(`Custom element name should have been valid but threw error: ${debugString(validName)} ${error.toString()}`);
    }
  }

  for (const invalidName of invalidCustomElementNames) {
    const newClass = createElementClass();
    assert_throws_dom(
      'SyntaxError',
      () => customElements.define(invalidName, newClass),
      `customElements.define should have thrown for invalid name: ${debugString(invalidName)}`);
    await promise_rejects_dom(t, 'SyntaxError',
      customElements.whenDefined(invalidName),
      `customElements.whenDefined should have thrown for invalid name: ${debugString(invalidName)}`);
  }
});
</script>
